package fr.cdesire.sudoku.core.commons;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.EnumSet;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import fr.cdesire.sudoku.core.enums.Valeur;
import fr.cdesire.sudoku.core.enums.Zone;
import fr.cdesire.sudoku.core.exceptions.FormatGrilleParametreException;
import fr.cdesire.sudoku.core.exceptions.SudokuCoreException;

/**
 * Représente le format d'une grille Sudoku déterminé par le nombres de cases d'une
 * ligne et le nombre de cases d'une colonne pour une région de la grille. A partir de ces 2 valeurs, 
 * cette classe calcule le nombre de cases qui constituera un côté de la grille donc le nombre de valeurs 
 * maximum que pourra prendre une case de cette grille. Elle initialise également un certains nombre 
 * de tableaux utilitaires permettant à partir d'une position de retrouver l'ensemble des positions 
 * constituant la ligne, la colonne ou la région (et donc l'ensemble des positions dépendantes à 
 * l'une d'entre elles).
 * @author cedric.desire@gmail.com
 */
public class FormatGrille {

    /**
     * Nombre minimal de cases pour une ligne ou une colonne d'une région.
     */
    public static final int MIN_NB_CASES_COTE_REGION = 2;
    /**
     * Nombre maximal de cases pour une ligne ou une colonne d'une région.
     */
    public static final int MAX_NB_CASES_COTE_REGION = 5;
    /**
     * Nombre de lignes des régions de la grille.
     */
    private final int nbLignesRegion;
    /**
     * Nombre de colonnes des régions la grille.
     */
    private final int nbColonnesRegion;
    /**
     * Nombre de cases formant le côté de la grille.
     */
    private final int nbCasesCoteGrille;
    /**
     * Nombre de cases dans la grille.
     */
    private final int nbCases;
    /**
     * Nombre de possibilité maximum des cases de la grille.
     */
    private final Set<Valeur> valeurs;
    /**
     * Ensemble des positions constituant la grille.
     */
    private final Set<Position> positions;
    /**
     * Permet la récupération d'une position à partir d'un numéro de ligne 
     * et d'un numéro de colonne
     */
    private final Map<ImmutableMap<Zone, Integer>, Position> positionParNumLigneNumColonne;
    /**
     * Association d'une ligne (ensemble de positions) avec son numéro.
     */
    private final Map<Integer, Set<Position>> lignes;
    /**
     * Association d'une colonne (ensemble de positions) avec son numéro.
     */
    private final Map<Integer, Set<Position>> colonnes;
    /**
     * Association d'une région (ensemble de positions) avec son numéro.
     */
    private final Map<Integer, Set<Position>> regions;
    /**
     * Liste des positions dépendantes d'une position (positions de la même ligne, de la même colonne et de la même région).
     */
    private final Map<Position, Set<Position>> positionsDependantes;

    /**
     * Constructeur privé : C'est lors de la création d'une grille qu'est calculé 
     * le format de la grille.
     * @param nbLignesRegion Nombre de cases formant la ligne d'une région.
     * @param nbColonnesRegion Nombre de cases formant la colonne d'une région.
     * @throws SudokuCoreException Levée d'une FormatGrilleParametreException si
     * les paramètres passés sont trop petits ou trop grands.
     */
    protected FormatGrille(final int nbLignesRegion, final int nbColonnesRegion) throws SudokuCoreException {

        controllerInput(nbLignesRegion, nbColonnesRegion);

        this.nbLignesRegion = nbLignesRegion;
        this.nbColonnesRegion = nbColonnesRegion;
        this.nbCasesCoteGrille = this.nbLignesRegion * this.nbColonnesRegion;
        this.nbCases = ((Double) Math.pow(this.nbCasesCoteGrille, 2)).intValue();
        
        this.valeurs = EnumSet.range(Valeur.UN, Valeur.values()[this.nbCasesCoteGrille - 1]);
        
        this.positions = initPositions();
        this.positionParNumLigneNumColonne = initPositionParNumLigneNumColonne();
        
        Map<Integer, Set<Position>> initLignes = new HashMap<Integer, Set<Position>>();
        Map<Integer, Set<Position>> initColonnes = new HashMap<Integer, Set<Position>>();
        Map<Integer, Set<Position>> initRegions = new HashMap<Integer, Set<Position>>();
        for (int numZone = 1; numZone <= nbCasesCoteGrille; numZone++) {
        	initLignes.put(numZone, initZone(Zone.LIGNE, numZone));
        	initColonnes.put(numZone, initZone(Zone.COLONNE, numZone));
        	initRegions.put(numZone, initZone(Zone.REGION, numZone));
        }
        
        this.lignes = initLignes;
        this.colonnes = initColonnes;
        this.regions = initRegions;
        
        this.positionsDependantes = initPositionsDependantes();
    }

    /**
     * Retourne le nombre de possibilités maximum des cases de la grille.
     * @return Nombre de possibilités maximum des cases de la grille.
     */
    public Set<Valeur> getValeurs() {
        return valeurs;
    }

    /**
     * Retourne le nombre de cases constituant la grille.
     * @return Nombre de cases constituant la grille.
     */
    public int getNbCases() {
        return nbCases;
    }

    /**
     * Retourne le nombre de colonnes de la grille.
     * @return Nombre de colonnes des régions de la grille.
     */
    public int getNbColonnesRegion() {
        return nbColonnesRegion;
    }

    /**
     * Retourne le nombre de cases constituant un côté de la grille de Sudoku.
     * @return Nombre de cases constituant un côté de la grille de Sudoku.
     */
    public int getNbCasesCoteGrille() {
        return nbCasesCoteGrille;
    }

    /**
     * Retourne le nombre de lignes des régions de la grille.
     * @return Nombre de lignes des régions de la grille.
     */
    public int getNbLignesRegion() {
        return nbLignesRegion;
    }

    /**
     * Retourne l'ensemble des positions constituant la grille.
     * @return Ensemble des positions constituant la grille.
     */
    public Set<Position> getPositions() {
        return positions;
    }

    /**
     * Association d'une ligne (ensemble de positions) avec son numéro.
     * @return L'ensemble de positions constituant la ligne.
     */
    public Map<Integer, Set<Position>> getLignes() {
        return lignes;
    }

    /**
     * Association d'une colonne (ensemble de positions) avec son numéro.
     * @return L'ensemble de positions constituant la colonne.
     */
    public Map<Integer, Set<Position>> getColonnes() {
        return colonnes;
    }

    /**
     * Association d'une région (ensemble de positions) avec son numéro.
     * @return L'ensemble de positions constituant la région.
     */
    public Map<Integer, Set<Position>> getRegions() {
        return regions;
    }

    /**
     * Retourne la liste des positions dépendantes à une position donnée (même ligne, même colonne et même région).
     * @return Liste des positions dépendantes à une position donnée.
     */
    public Map<Position, Set<Position>> getPositionsDependantes() {
        return positionsDependantes;
    }

    /**
     * Retourne la position de la grille en fonction du numéro de ligne et du numéro de colonne.
     * @param numeroLigne Numéro de ligne.
     * @param numeroColonne Numéro de colonne.
     * @return Position de la grille
     */
    public Position getPosition(final int numeroLigne, final int numeroColonne) {
        return positionParNumLigneNumColonne.get(ImmutableMap.of(Zone.LIGNE, numeroLigne, Zone.COLONNE, numeroColonne));
    }

    /**
     * Création d'un tableau recensant l'ensemble des positions constituant la grille.
     * @return Ensemble de positions constituant la grille.
     */
    private Set<Position> initPositions() {

        Set<Position> init = new HashSet<Position>();

        for (int i = 1; i <= nbCasesCoteGrille; i++) {
            for (int j = 1; j <= nbCasesCoteGrille; j++) {
                init.add(new Position(i, j, calculRegion(i, j)));
            }
        }

        return init;
    }

    /**
     * Création d'un tableau permettant la récupération d'une position à partir d'un numéro de ligne 
     * et d'un numéro de colonne
     */
    private Map<ImmutableMap<Zone, Integer>, Position> initPositionParNumLigneNumColonne() {

        Map<ImmutableMap<Zone, Integer>, Position> init = Maps.newHashMap();

        for (Position position : positions) {
            init.put(ImmutableMap.of(Zone.LIGNE, position.getLigne(), Zone.COLONNE, position.getColonne()), position);
        }

        return init;
    }
    
    /**
     * Récupération des positions d'une zone en fonction de son numéro.
     * @param zone Type de la zone recherchée (ligne, colonne ou région).
     * @param numZone Numéro de la zone recherchée.
     * @return Liste des positions de la zone recherchée.
     */
    private Set<Position> initZone(final Zone zone, final int numZone) {

        return Sets.filter(this.positions, new Predicate<Position>() {

            @Override
            public boolean apply(Position filtre) {
            	
            	int zoneFiltree = 0;
            	
            	switch (zone) {
				case LIGNE:
					zoneFiltree = filtre.getLigne();
					break;

				case COLONNE:
					zoneFiltree = filtre.getColonne();
					break;
					
				case REGION:
					zoneFiltree = filtre.getRegion();
            	}
            	
                if (numZone != zoneFiltree) {
                    return false;
                }
                return true;
            }
        });
    }

    /**
     * Création de l'association entre une position et la liste de positions dépendantes à celle-ci.
     * @return Association d'une position avec sa liste de positions dépendantes.
     */
    private Map<Position, Set<Position>> initPositionsDependantes() {

        Map<Position, Set<Position>> init = new HashMap<Position, Set<Position>>();

        for (Position position : positions) {
            Set<Position> liste = new HashSet<Position>();
            liste.addAll(lignes.get(position.getLigne()));
            liste.addAll(colonnes.get(position.getColonne()));
            liste.addAll(regions.get(position.getRegion()));
            liste.remove(position);

            init.put(position, liste);
        }

        return init;
    }

    /**
     * Calcul du numéro de la région en fonction du numéro de la position (déterminé 
     * par un numéro de ligne et un numéro de colonne). Ce numéro s'incrémente dans l'ordre
     * de gauche à droite puis de haut en bas.
     * @param ligne Numéro de ligne.
     * @param colonne Numéro de colonne.
     * @return numéro de la région calculée.
     */
    private int calculRegion(final int ligne, final int colonne) {

        final int posRegionSurLigne = (colonne - 1) / nbColonnesRegion + 1;
        final int posRegionSurColonne = (ligne - 1) / nbLignesRegion + 1;

        return (posRegionSurColonne - 1) * nbLignesRegion + posRegionSurLigne;
    }

    /**
     * Contrôle des paramètres passés au constructeur.
     * @param nbLignesRegion Nombre de lignes des régions de la grille.
     * @param nbColonnesRegion Nombre de colonnes des régions de la grille.
     * @throws SudokuCoreException Levée d'une FormatGrilleParametreException si
     * les paramètres passés sont trop petits ou trop grands.
     */
    private void controllerInput(final int nbLignesRegion, final int nbColonnesRegion) throws SudokuCoreException {

        if ((nbLignesRegion < MIN_NB_CASES_COTE_REGION) || (nbLignesRegion > MAX_NB_CASES_COTE_REGION)
        		|| (nbColonnesRegion < MIN_NB_CASES_COTE_REGION) || (nbColonnesRegion > MAX_NB_CASES_COTE_REGION)) {
            throw new FormatGrilleParametreException();
        }
    }
}
